昨天,我們已經把 Framework 最核心的流程接起來:
HttpRequest
↓
Router
↓
Handler
↓
HttpResponse
也就是說,Request 進來後,Framework 已經知道怎麼找到 Handler,執行它,最後把結果變成 Response。
但前面 Day24 研究 Middleware 時,我們已經知道:
真正的 Web Framework 不只是在 Request 到達 Handler 時做 Routing。
很多 Request 還會需要經過一些共用處理。
例如:
Logging
Exception Handling
Authentication
Timing
CORS
如果這些功能全部寫在每一個 Handler 裡,程式會變得非常重複。
所以今天要做的事情,就是把之前只停留在概念上的 Middleware,真正放進我們的 Framework Flow 裡。
今天想理解的問題就是:
Framework 到底怎麼讓每一個 Request 在到達 Handler 前後,自動經過同一套 Middleware?
💭 我原本以為……
以前看到 ASP.NET Core:
app.Use(...)
會覺得這可能只是:
幫 Framework 開啟某個功能。
例如:
UseAuthentication
就是「開啟驗證」。
UseExceptionHandler
就是「開啟錯誤處理」。
但前面理解 Middleware 後才知道:
這些東西不是單純設定值。
它們其實是在建立:
Request Pipeline。
Request 不會直接跳到 Handler。
而是會依序經過不同 Middleware。
例如:
Request
↓
Logging
↓
Authentication
↓
Routing
↓
Handler
Handler 完成後,流程又會往回走:
Handler
↓
Authentication
↓
Logging
↓
Response
所以 Middleware 真正改變的,不是某個單一功能。
而是:
Request 的整體執行流程。
目前 Framework 的流程少了什麼?
Day27 的核心大概是:
Browser
↓
MiniWebServer
↓
HttpRequest
↓
Router
↓
Handler
↓
HttpResponse
↓
Browser
這已經可以工作。
但如果我想在每個 Request 開始時印:
收到 GET /hello
然後完成後再印:
GET /hello 完成
現在最簡單的方法可能就是直接寫在 MiniWebServer。
但如果未來還要:
計時
驗證
錯誤處理
全部放進 MiniWebServer,就會再次回到:
一個 Class 什麼都負責。
這和 Day26 想拆掉巨大 Program.cs 的目的完全相反。
所以這些共用工作應該有自己的位置。
這個位置就是:
Middleware Pipeline。
第一個 Middleware 先做什麼?
最後成品不用一開始就做 Authentication。
最適合的第一個 Middleware 還是:
Logging Middleware。
因為它很簡單,又很容易看出 Pipeline 有沒有真的運作。
想像 Request:
GET /hello
進入 Pipeline 後:
Logging Middleware
↓
印出:
Request 開始:GET /hello
↓
next
↓
Router
↓
Handler
↓
HttpResponse
↓
回到 Logging Middleware
↓
印出:
Request 完成:GET /hello
如果能看到這個順序,就代表 Middleware 已經真正「包住」Handler。
next 再次出現
Day24 已經提過 next。
今天正式把它放進 Framework 時,可以再重新理解。
next 不是一個特殊的 HTTP 關鍵字。
它本質上只是:
下一段可以被執行的程式。
例如 Middleware 可以理解成:
先做自己的事
↓
呼叫 next()
↓
等 next 完成
↓
再做自己的事
所以:
Middleware A
↓
next
如果 next 指向 Middleware B:
A Before
↓
B Before
↓
Handler
↓
B After
↓
A After
也就是:
A(
B(
Handler
)
)
所以 Middleware Pipeline 真正做的事情,其實是:
一層一層把 Delegate 包起來。
Delegate 又接回來了
Day13 做 Routing 時,我們第一次接觸:
Action
Func
當時只是把 Handler 當成:
可以稍後執行的一段程式。
到了 Middleware,又會看到同一個概念。
例如:
next
就是:
可以稍後呼叫的下一段流程。
所以整個 Framework Pipeline 可以透過 Delegate 來組合。
這也讓我發現:
Delegate 不只是 C# 裡一個語法功能。
對 Framework 來說,它其實非常重要。
因為 Framework 常常需要:
先「保存一段程式」
等到某個 Request 發生時
再按照流程呼叫。
Middleware 的基本結構可以怎麼想?
目前不一定要先寫實際程式,但概念可以是:
Middleware
↓
接收 HttpRequest
↓
接收 next
↓
做 Before
↓
呼叫 next
↓
得到 HttpResponse
↓
做 After
↓
回傳 HttpResponse
也就是:
HttpRequest
↓
Middleware
↓
HttpResponse
但是 Middleware 中間還可以呼叫:
next
讓 Request 繼續往下一層。
最終最底層的 next 就可能是:
Router
↓
Handler
↓
HttpResponse
這樣 Middleware 就不需要自己知道 Route Table 裡有哪些 Handler。
它只需要:
處理 Request
然後決定要不要繼續。
Request Pipeline 怎麼被組起來?
假設我們有:
Logging Middleware
Exception Middleware
最後的 Handler Pipeline
Framework 要把它們組成:
Exception
↓
Logging
↓
Handler
可以先想成:
最底層:
Handler
再包一層:
Logging(Handler)
最後再包:
Exception(
Logging(
Handler
)
)
Request 進去後:
Exception Before
↓
Logging Before
↓
Handler
↓
Logging After
↓
Exception After
所以 Pipeline Builder 的工作其實就是:
把一堆 Middleware 按照順序組合成一個最終 Delegate。
最後 Server 不需要知道裡面有幾層。
它只需要:
pipeline(request)
就可以了。
這是今天我覺得非常重要的轉變。
Server 原本:
收到 Request
↓
自己 Routing
↓
自己執行 Handler
現在可以變成:
收到 Request
↓
交給 Pipeline
至於 Pipeline 裡:
Logging
Routing
Handler
Exception Handling
怎麼執行,
由 Framework 自己管理。
Middleware 為什麼可以 Short-Circuit?
假設 Middleware 不呼叫 next。
那後面的流程自然就不會發生。
例如:
Authentication Middleware
↓
發現沒有 Token
↓
直接建立 401 Response
↓
return
這次就不會:
Router
↓
Handler
所以 Middleware 本身有能力決定:
Request 還能不能繼續。
這就是 Short-Circuit。
對我們最後的 Mini Web Framework 來說,不一定要做完整 Authentication。
但如果 Middleware 架構設計正確,理論上就已經支援這種能力。
這就是 Framework Design 很有趣的地方。
今天只做 Logging。
但好的設計可以讓未來:
Authentication
Exception Handling
Timing
也使用同一套 Pipeline 機制。
Logging Middleware 需要知道 Router 嗎?
不需要。
它只需要知道 Request。
例如:
Method = GET
Path = /hello
然後:
開始時間
next()
結束時間
它根本不用知道:
最後是哪個 Handler。
也不用知道:
Route Table 長什麼樣。
這就是責任分離又再次出現。
Router 不需要知道 Logging。
Logging 也不需要知道 Router。
它們只是透過 Pipeline 串起來。
這比把:
Console.WriteLine()
直接塞進 Router 或 Server 乾淨很多。
Exception Middleware 又可以怎麼接?
雖然最後成品時間有限,但可以先理解它的位置。
例如:
Exception Middleware
↓
try
↓
next()
↓
正常 HttpResponse
如果:
next()
↓
Handler
↓
throw Exception
就會回到:
catch
然後產生:
500 Internal Server Error
所以:
Exception Middleware
↓
包住整個後續 Pipeline
這個設計可以讓:
Router
Handler
Result Converter
後面發生的錯誤,
統一在外層處理。
這也是 Middleware Pipeline 很強的地方。
順序為什麼又重要?
假設:
Exception Middleware
放在 Logging Middleware 外面:
Exception
↓
Logging
↓
Handler
如果 Logging 本身也發生 Exception,
外層 Exception Middleware 有機會捕捉到。
但如果順序反過來:
Logging
↓
Exception
↓
Handler
那 Logging 自己前置處理產生的錯誤,內層 Exception Middleware 就抓不到。
所以 Middleware 的註冊順序,不只是看起來好不好看。
它會真正改變:
誰包住誰
以及:
誰能處理誰的結果。
這也就是 ASP.NET Core Middleware 順序非常重要的原因。
Program.cs 又更像 Framework 使用端了
如果 Middleware 真的做進來,
Program.cs 未來可以開始像:
建立 Application
加入 Logging Middleware
註冊 Route
Run
而不用:
手動呼叫 Logging
手動呼叫 Router
手動 Send
所以理想的使用方式開始變成:
Application Setup
↓
Framework 執行
這又再次接回:
Inversion of Control。
開發者不是每次 Request 來都自己控制整個流程。
而是先告訴 Framework:
我要哪些 Middleware
我要哪些 Route
接著 Framework 自己:
等 Request
跑 Pipeline
執行 Handler
產生 Response
官方怎麼說?
ASP.NET Core 的 Request Pipeline 是由一連串 Request Delegates 組成。
每個 Middleware 可以:
在下一個元件之前執行工作
呼叫下一個 Middleware
在下一個 Middleware 完成後再執行工作
或者不呼叫下一個元件,直接 Short-Circuit Pipeline。
所以:
Request
↓
Middleware A
↓
Middleware B
↓
Endpoint
↓
Response
不是單純由上往下的一串函式。
更準確地說,是一層一層組合起來的 Request Delegates。
這和我們今天想讓 Mini Web Framework 做的最小 Middleware Pipeline 是同一個核心概念。
以前的我:
Middleware 就是 Framework 裡的一個額外功能。
現在的我:
Middleware
↓
其實是 Request Pipeline 的組成單位
Framework
↓
把 Middleware 一層一層組合
↓
形成最終 Pipeline
↓
Server 把每一個 HttpRequest 丟進 Pipeline
也就是:
Middleware 不只是「做 Logging」。
真正重要的是:
它提供一種可以插入共用處理的 Framework 機制。
今天最大的發現是:
Pipeline 本身也可以被看成「一段程式」。
Server 不需要知道:
Logging
Authentication
Router
Handler
分別怎麼跑。
只要 Framework 事先把它們組成:
pipeline
Request 進來後:
pipeline(request)
所有流程就會按照順序發生。
這讓 Framework 又多了一層抽象:
以前:
Server
↓
直接控制每一步
現在:
Server
↓
Pipeline
↓
Pipeline 自己控制每一步
這開始真正有 Framework 的味道了。
Framework 可以把每個 Middleware 看成一個會包住「下一段流程」的 Delegate。
從最底層 Handler 開始,一層一層把 Middleware 包起來,最後形成一個完整 Pipeline。
Server 收到 Request 後,只需要把 HttpRequest 交給 Pipeline。
剩下的 Logging、Routing、Handler、Response 等流程,就由 Framework 自己執行。
🛠 與 Mini Web Framework 的關聯
Day27 的 Framework Core:
MiniWebServer
↓
HttpRequest
↓
Router
↓
Handler
↓
HttpResponse
Day28 加入 Middleware 後:
MiniWebServer
↓
HttpRequest
↓
Middleware Pipeline
│
├── Logging
│
├── 其他 Middleware
│
└── Router
↓
Handler
↓
HttpResponse
↓
Browser
這代表 Mini Web Framework 開始真正具有:
Request Pipeline。
而不是只有:
Route Table + TCP Server。
最後成品至少可以做到:
啟動 Server
註冊 Route
使用 Logging Middleware
Request 自動經過 Middleware
找到 Handler
產生 HttpResponse
回到 Browser
這樣已經足以展示一個 Mini Web Framework 的核心架構。
今天的實作目標
Day28 真正補實作時,可以先只做最小 Logging Middleware。
例如每次 Request:
Request 開始:GET /hello
↓
執行後面的 Pipeline
↓
Request 完成:GET /hello
只要 Console 順序可以證明:
Middleware Before
↓
Handler
↓
Middleware After
就代表 Middleware Pipeline 已經成功。
不需要今天就做完整 Authentication 或 CORS。
因為最後成品最重要的是:
核心架構完整。
而不是功能數量最多。
結尾
前面幾天,我們一直在做不同的 Framework 元件。
HttpRequest
Router
Handler
HttpResponse
但今天第一次開始處理:
這些元件「怎麼按照順序執行」。
Middleware 讓我發現:
Framework 的設計不只是「有哪些 Class」。
還包括:
控制流程本身也可以被抽象化。
Request 進來後,不需要由 MiniWebServer 手動寫:
先 Logging
再 Routing
再 Handler
而是 Framework 可以事先把它們組成:
Pipeline
最後 Server 只需要:
把 Request 交給 Pipeline。
這讓整個架構又往前一步。
從一個可以處理 HTTP 的 Server,
逐漸變成:
一個可以組合 Request Processing 行為的 Framework。
現在 Mini Web Framework 已經有:
Server
HttpRequest
Router
Handler
HttpResponse
Middleware Pipeline
核心架構幾乎都到位了。
但最後還不能只看:
/hello 有沒有顯示 Hello。
真正的成品還需要確認:
/products 正不正常?
不存在的 Route 有沒有 404?
連續送很多 Request 會不會出問題?
Middleware 每次都有沒有執行?
如果 Handler 出錯又會怎樣?
所以最後兩天,就不能再只增加新東西了。